import {
  Action, fromBody, fromJwt,
  fromParam,
  InjectPermissions, JwtPayload, JwtPayloadDto,
  Permission,
  PermissionsDecoratorOptions,
  PermissionsTo,
} from '@boticord/auth'
import { BitFieldUtil } from '@boticord/common'
import { ErrorCodes } from '@boticord/consts'
import {
  Bot,
  ChangeResourceStatusDto,
  CreateBotDto,
  PendingBot,
  ResourceStatus,
  UpdateBotDto,
  UpdateBotStatsDto,
} from '@boticord/resources-client'
import {
  BadRequestException,
  Body,
  Controller,
  Delete, ForbiddenException,
  Get, InternalServerErrorException,
  NotFoundException,
  Param,
  Patch,
  Post, Req,
} from '@nestjs/common'
import { BotsService } from '../services/bots.service'
import { User } from '@boticord/users-client'
import { BotClientService } from '@boticord/bot-client'
import { AnalyseRequest, AnalyticsOptions, EventTypes, getIp } from '@boticord/analytics'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { FastifyRequest } from 'fastify'
import { ChangePendingBotStatusDto } from '@boticord/resources-client/dto/change-pending-bot-status.dto'

const permissionsOptions: PermissionsDecoratorOptions<Bot | PendingBot> = {
  fetch: {
    id: fromParam('id'),
    exception: new NotFoundException({ code: ErrorCodes.UnknownBot }),
    options: {
       relations: {
         developers: true,
       },
    },
  },
}

const analyticsOptions: AnalyticsOptions = {
  user: fromJwt('id'),
  affected: fromParam('id'),
}

@Controller('bots')
export class BotController {
  constructor(
    private readonly bots: BotsService,
    private readonly bot: BotClientService,
    @InjectRepository(Bot) private readonly botRepo: Repository<Bot>,
    @InjectRepository(PendingBot) private readonly pendingBotRepo: Repository<PendingBot>,
  ) {}

  @Get(':id')
  @PermissionsTo(Action.Read, Bot, permissionsOptions)
  @AnalyseRequest(EventTypes.BotViewed, analyticsOptions)
  async getBot(@Param('id') botID: string, @JwtPayload() jwt?: JwtPayloadDto, @InjectPermissions() perms?: number) {
    return await this.bots.publicGet(botID, jwt?.id, perms)
  }

  @Get('/pending/:id')
  @PermissionsTo(Action.Read, PendingBot, permissionsOptions)
  @AnalyseRequest(EventTypes.PendingBotViewed, analyticsOptions)
  async getPendingBot(@Param('id') botID: string) {
    return await this.bots.getPending(botID)
  }

  @Get('/pending')
  @PermissionsTo(Action.ReadMany, PendingBot)
  @AnalyseRequest(EventTypes.PendingBotsViewed, analyticsOptions)
  async getPendingBots() {
    return await this.bots.getPendingBots()
  }

  @Get('/blocked')
  @PermissionsTo(Action.ReadMany, Bot)
  @AnalyseRequest(EventTypes.BlockedBotsViewed, analyticsOptions)
  async getBlockedBots() {
    return await this.bots.getBlockedBots()
  }

  @Delete(':id')
  @PermissionsTo(Action.Delete, Bot, permissionsOptions)
  @AnalyseRequest(EventTypes.BotRemoved, analyticsOptions)
  async deleteBot(@Param('id') botID: string) {
    await this.bots.delete(botID)
    return true
  }

  @Delete('/pending/:id')
  @PermissionsTo(Action.Delete, PendingBot, permissionsOptions)
  @AnalyseRequest(EventTypes.PendingBotRemoved, analyticsOptions)
  async deletePendingBot(@Param('id') botID: string) {
    await this.bots.delete(botID, true)
    return true
  }

  @Post('create')
  @PermissionsTo(Action.Create, PendingBot)
  @AnalyseRequest(EventTypes.BotAdded, { user: fromJwt('id'), affected: fromBody('id') })
  async createBot(
    @Body() dto: CreateBotDto,
    @InjectPermissions() permissions: number,
    @JwtPayload() jwt: JwtPayloadDto,
    @Req() req: FastifyRequest,
  ) {
    const ip = getIp(req.headers)
    if (!ip) throw new InternalServerErrorException({ code: ErrorCodes.CannotDetectIp })
    return await this.bots.createBot(jwt.id, dto, permissions, ip)
  }

  @Patch(':id')
  @PermissionsTo(Action.Modify, Bot, permissionsOptions)
  @AnalyseRequest(EventTypes.BotEdited, analyticsOptions)
  async modifyBot(
    @Param('id') botID: string,
    @Body() dto: UpdateBotDto,
    @InjectPermissions() permissions: number,
    @JwtPayload() jwt: JwtPayloadDto,
  ) {
    return await this.bots.modifyBot(botID, jwt.id, dto, permissions)
  }

  @Patch('/pending/:id')
  @PermissionsTo(Action.Modify, PendingBot, permissionsOptions)
  @AnalyseRequest(EventTypes.PendingBotEdited, analyticsOptions)
  async modifyPendingBot(
    @Param('id') botID: string,
    @Body() dto: UpdateBotDto,
    @InjectPermissions() permissions: number,
    @JwtPayload() jwt: JwtPayloadDto,
  ) {
    return await this.bots.modifyPendingBot(botID, jwt.id, dto, permissions)
  }

  @Get(':id/discord')
  @PermissionsTo(Action.Read, User)
  @AnalyseRequest(EventTypes.TriedToAddBot, analyticsOptions)
  async getBotApplication(
    @Param('id') botID: string, @InjectPermissions() permissions: number, @JwtPayload() jwt: JwtPayloadDto,
  ) {
    const bot = await this.botRepo.findOne({ where: { id: botID } })
    if (bot) throw new NotFoundException({ code: ErrorCodes.DuplicateBot })

    const pendingBot = await this.pendingBotRepo.findOne({ where: { id: `${jwt.id}+${botID}` } })
    if (pendingBot) throw new NotFoundException({ code: ErrorCodes.DuplicateBot })

    const app = await this.bot.getUser(botID)
    if (!app?.bot) throw new NotFoundException({ code: ErrorCodes.UnknownApplication })

    if (!BitFieldUtil.has(permissions, Permission.ReadDiscordApplication)) {
      return app
    }

    const resp = await this.bots.fetchBotApp(botID)
    if (!resp || !resp?.application) return app

    return {
      ...resp.application,
      ...app,
    }
  }

  @Post('/pending/:id/approve')
  @PermissionsTo(Action.ChangeStatus, PendingBot, permissionsOptions)
  @AnalyseRequest(EventTypes.BotApproved, analyticsOptions)
  async approve(@Param('id') id: string) {
    return await this.bots.approveBot(id)
  }

  // only bots can post stats
  @Post(':id/stats')
  @PermissionsTo(Action.Modify, Bot, { ...permissionsOptions, token: 'bot' })
  async postStats(@Param('id') botID: string, @Body() dto: UpdateBotStatsDto) {
    return await this.bots.modifyStats(botID, dto)
  }

  @Patch(':id/status')
  @PermissionsTo(Action.ChangeStatus, Bot, permissionsOptions)
  async modifyStatus(
    @Param('id') botID: string,
    @Body() dto: ChangeResourceStatusDto,
    @InjectPermissions() permissions: number,
  ) {
    if (
      [ ResourceStatus.Banned, ResourceStatus.Pending ].includes(dto.status) && 
      !BitFieldUtil.has(permissions, Permission.ModerateBots)
    ) throw new BadRequestException({ code: ErrorCodes.InvalidStatus })
    return await this.bots.changeStatus(botID, dto.status)
  }

  @Patch(':id/pending/status')
  @PermissionsTo(Action.ChangeStatus, PendingBot, permissionsOptions)
  async modifyPendingBotStatus(
    @Param('id') botID: string,
    @Body() dto: ChangePendingBotStatusDto,
  ) {
    return await this.bots.changePendingBotStatus(botID, dto.status)
  }
}
